﻿using System;
using System.Reflection;
using System.Xml.Serialization;
using System.Collections.Generic;

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Graphics;

using FarseerPhysics.Dynamics;
using FarseerPhysics.Common;
using FarseerPhysics.Factories;
using FarseerPhysics.Dynamics.Contacts;

namespace Spacer {
	public enum MovementMode {
		None,
		Normal,
		Projectile // No gravity | bullet
	}

	// ----------------------------------------------------------------------------------------------------

	public class Entity : Drawable {
		// TODO: Replace all manual save state reconstruction to use this function
		public static Entity CreateFromSaveState( EntitySaveState state, SolarSys world, bool no_postload ) {
			Entity ent = (Entity)Activator.CreateInstance( state.EntityType );
			ent.World = world;
			ent.ID = (state.EntityID != Guid.Empty ? state.EntityID : world.GetNextID());
			ent.LoadSaveState( state );

			if( !no_postload ) {
				ent.PostLoad();
				ent.LoadedSaveState = null;
			}

			return ent;
		}

		// ----------

		public Entity() {}

		public Entity( SolarSys world ) : base(world) {
			if( this.GetType() == typeof(Entity) ) Initialize();
		}

		public override void Initialize() {
			base.Initialize();

			// Generate GUID
			if( ID == Guid.Empty ) {
				ID = World.GetNextID();
			}
			
			World.AddedEntities.Add( ID, this );

			Health = 100f;
			MaxHealth = 100f;
		}

		// ----------

		#region Saving
		public virtual bool CreateSaveState( EntitySaveState save ) {
			save["TextureName"] = TextureName;
			save["SpriteName"] = SpriteName;
			save["DrawMode"] = DrawMode;
			save["DrawColor"] = DrawColor;
			save["DrawScale"] = DrawScale;
			save["DrawOrigin"] = DrawOrigin;
			save["Sequence"] = Sequence;
			save["AnimationMode"] = AnimationMode;
			save["AnimationScale"] = AnimationScale;
			save["FrameCurrent"] = FrameCurrent;

			save["Position"] = Position;
			save["Velocity"] = Velocity;
			save["Acceleration"] = Acceleration;
			save["Angle"] = Angle;
			save["AngularVelocity"] = AngularVelocity;
			save["AngularAcceleration"] = AngularAcceleration;

			save["OwnerID"] = OwnerID;
			save["ParentID"] = ParentID;

			save["Health"] = Health;

			save["CreationTime"] = CreationTime;
			save["LifeTime"] = LifeTime;

			return true;
		}

		public virtual void LoadSaveState( EntitySaveState save ) {
			Type type = GetType();
			PropertyInfo prop = null;

			LoadedSaveState = save;

			// Some init functions require values to be set first such as a saved randomized number
			foreach(KeyValuePair<string,object> data in save.Data) {
				if( (prop = type.GetProperty(data.Key,BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy)) != null ) {
					prop.SetValue( this, data.Value, null );
				}
			}

			Initialize();

			// Force new values
			foreach(KeyValuePair<string,object> data in save.Data) {
				if( (prop = type.GetProperty(data.Key,BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy)) != null ) {
					prop.SetValue( this, data.Value, null );
				}
			}
		}

		public virtual void PostLoad() {
			if( (Guid)LoadedSaveState["OwnerID"] != Guid.Empty )
				OwnerID = (Guid)LoadedSaveState["OwnerID"];

			if( (Guid)LoadedSaveState["ParentID"] != Guid.Empty )
				ParentID = (Guid)LoadedSaveState["ParentID"];
		}
		#endregion

		#region Physics Initialization
		public void PhysicsInit( float density, MovementMode physmode ) {
			if( PhysicsObject != null ) {
				World.Instance.RemoveBody( PhysicsObject );
			}

			string key = "";
            List<Vertices> mesh = null;
			bool add_to_cache = false;

			if( DrawMode == DrawMode.Sprite )
				key = Sprite.Name;
			else
				key = Texture.Name;

			if( !Main.PhysicsMeshes.TryGetValue(key,out mesh) ) {
				add_to_cache = true;
				
				Texture2D tex;
				int tex_width, tex_height;
				int tex_offsetw, tex_offseth;
				uint[] data;

				if( DrawMode == DrawMode.Sprite ) {
					tex = Sprite.MaskTexture;
					if( tex == null )
						tex = Sprite.Texture;
					if( tex == null )
						return;

					tex_width = (int)Sprite.Size.X;
					tex_height = (int)Sprite.Size.Y;
					tex_offsetw = (int)Sprite.Center.X;
					tex_offseth = (int)Sprite.Center.Y;

					data = new uint[(int)Sprite.Size.X * (int)Sprite.Size.Y];
					tex.GetData( 0, Sprite.GetFrameRect(0), data, 0, data.Length );
				} else if( DrawMode == DrawMode.Texture ) {
					tex = MaskTexture;
					if( tex == null )
						tex = Texture;
					if( tex == null )
						return;

					tex_width = tex.Width;
					tex_height = tex.Height;
					tex_offsetw = 0;
					tex_offseth = tex.Height;

					data = new uint[tex.Width * tex.Height];
					tex.GetData( 0, null, data, 0, data.Length );
				} else {
					return;
				}

				// Create polygon from image
				Vertices verts = PolygonTools.CreatePolygon( data, tex_width, false );

				// Flip to match screen
				Vector2 mirror = new Vector2( 1f, -1f );
				verts.Scale( ref mirror );

				// Translate center
				verts.Translate( new Vector2(-tex_offsetw,tex_offseth) );

				// Scale to world
				Vector2 scale = new Vector2( 1f/Surface.WorldScale, 1f/Surface.WorldScale );
				verts.Scale( ref scale );

				// User scale
				Vector2 drawscale = DrawScale;
				verts.Scale( ref drawscale );

				try
				{
					//mesh = FarseerPhysics.Common.Decomposition.BayazitDecomposer.ConvexPartition(verts);
					mesh = FarseerPhysics.Common.Decomposition.EarclipDecomposer.ConvexPartition( verts );
				}
				catch (Exception e)
				{
					Console.WriteLine(e.Message);
					return;
				}
			}

			PhysicsObject = BodyFactory.CreateCompoundPolygon( World.Instance, mesh, density );
			PhysicsObject.Position = base.Position;
			PhysicsObject.LinearVelocity = base.Velocity;
			PhysicsObject.Rotation = base.Angle;
			PhysicsObject.AngularVelocity = base.AngularVelocity;
			PhysicsObject.LinearDamping = base.LinearDamping;
			PhysicsObject.AngularDamping = base.AngularDamping;

			if( PhysicsObject == null ) {
				return;
			}// else if( add_to_cache ) {
			//	if( DrawMode == DrawMode.Sprite )
			//		Main.PhysicsMeshes[Sprite.Name] = mesh;
			//	else
			//		Main.PhysicsMeshes[Texture.Name] = mesh;
			//}

			PhysicsObject.UserData = this;

			// Set body type according to MovementMode provided
			if( physmode == MovementMode.None ) {
				PhysicsObject.BodyType = BodyType.Static;
			} else {
				PhysicsObject.BodyType = BodyType.Dynamic;
				if( physmode == MovementMode.Projectile ) {
					PhysicsObject.IgnoreGravity = true;
					PhysicsObject.IsBullet = true;
				}
			}

			PhysicsObject.OnCollision += new OnCollisionEventHandler( InternalOnCollision );
			PhysicsObject.OnSeparation += new OnSeparationEventHandler( InternalOnSeparation );
			
			// Enable and wake
			PhysicsObject.Enabled = true;
			PhysicsObject.Awake = true;
		}

		public void PhysicsInitBox( float width, float height, MovementMode physmode ) {
			if( PhysicsObject != null ) {
				World.Instance.RemoveBody( PhysicsObject );
			}

			PhysicsObject = BodyFactory.CreateRectangle( World.Instance, width, height, 1f, Position );
			PhysicsObject.Position = base.Position;
			PhysicsObject.LinearVelocity = base.Velocity;
			PhysicsObject.Rotation = base.Angle;
			PhysicsObject.AngularVelocity = base.AngularVelocity;
			PhysicsObject.LinearDamping = base.LinearDamping;
			PhysicsObject.AngularDamping = base.AngularDamping;

			if( PhysicsObject == null )
				return;
			PhysicsObject.UserData = this;

			// Set body type according to MovementMode provided
			if( physmode == MovementMode.None ) {
				PhysicsObject.BodyType = BodyType.Static;
			} else {
				PhysicsObject.BodyType = BodyType.Dynamic;
				if( physmode == MovementMode.Projectile ) {
					PhysicsObject.IgnoreGravity = true;
					PhysicsObject.IsBullet = true;
				}
			}

			PhysicsObject.OnCollision += new OnCollisionEventHandler( InternalOnCollision );
			PhysicsObject.OnSeparation += new OnSeparationEventHandler( InternalOnSeparation );
			
			// Enable and wake
			PhysicsObject.Enabled = true;
			PhysicsObject.Awake = true;
		}

		public void PhysicsInitCircle( float radius, MovementMode physmode ) {
			if( PhysicsObject != null ) {
				World.Instance.RemoveBody( PhysicsObject );
			}

			PhysicsObject = BodyFactory.CreateCircle( World.Instance, radius, 1f, Position );
			PhysicsObject.Position = base.Position;
			PhysicsObject.LinearVelocity = base.Velocity;
			PhysicsObject.Rotation = base.Angle;
			PhysicsObject.AngularVelocity = base.AngularVelocity;
			PhysicsObject.LinearDamping = base.LinearDamping;
			PhysicsObject.AngularDamping = base.AngularDamping;

			if( PhysicsObject == null )
				return;
			PhysicsObject.UserData = this;
			
			// Set body type according to MovementMode provided
			if( physmode == MovementMode.None ) {
				PhysicsObject.BodyType = BodyType.Static;
			} else {
				PhysicsObject.BodyType = BodyType.Dynamic;
				if( physmode == MovementMode.Projectile ) {
					PhysicsObject.IgnoreGravity = true;
					PhysicsObject.IsBullet = true;
				}
			}

			PhysicsObject.OnCollision += new OnCollisionEventHandler( InternalOnCollision );
			PhysicsObject.OnSeparation += new OnSeparationEventHandler( InternalOnSeparation );
			
			// Enable and wake
			PhysicsObject.Enabled = true;
			PhysicsObject.Awake = true;
		}
		#endregion

		#region Updating
		public override void Update() {
			// !!! - DO NOT CALL base.Update() FROM HERE - !!! //
			// !!! - ONLY IN ENTITY CHILD CLASSES		- !!! //

			// Death after lifetime
			if( (LifeTime != 0f && Globals.GameTime >= CreationTime + LifeTime) || (Parent != null && Parent.IsMarkedForRemoval) || (Owner != null && Owner.IsMarkedForRemoval)   )
				Remove();

			if( Parent != null && PhysicsObject == null ) {
				Position = Parent.Position + Vector2.Transform( LocalPosition, Matrix.CreateRotationZ(Parent.Angle) );
				Angle = Parent.Angle + LocalAngle; // TODO: Is this right?
			} else {
				Velocity += Acceleration * Globals.UpdateDeltaTime;
				if( PhysicsObject == null ) {
					Velocity *= (1f - (LinearDamping * Globals.UpdateDeltaTime));
					Position += Velocity * Globals.UpdateDeltaTime;
				}
				
				AngularVelocity += AngularAcceleration * Globals.UpdateDeltaTime;
				if( PhysicsObject == null ) {
					AngularVelocity *= (1f - (AngularDamping * Globals.UpdateDeltaTime));
					Angle += AngularVelocity * Globals.UpdateDeltaTime;
				}
			}
		}

		// TODO: Does nothing right now.
		public virtual void UpdateAgainst( Entity other ) {}
		#endregion

		#region Collision Handling
		public bool InternalOnCollision( Fixture fixtureA, Fixture fixtureB, Contact contact ) {
			if( fixtureA.Body.UserData == null || fixtureB.Body.UserData == null )
				return true;

			Entity entA = (Entity)fixtureA.Body.UserData;
			Entity entB = (Entity)fixtureB.Body.UserData;

			if( entA.Owner == entB || entB.Owner == entA )
				return false;

			if( entA == this )
				return OnCollision( entB, contact );
			else if( entB == this )
				return OnCollision( entA, contact );

			return true;
		}

		public void InternalOnSeparation( Fixture fixtureA, Fixture fixtureB ) {
			if( fixtureA.Body.UserData == null || fixtureB.Body.UserData == null )
				return;

			if( fixtureA.Body == PhysicsObject )
				OnSeparation( (Entity)fixtureB.Body.UserData );
			else if( fixtureB.Body == PhysicsObject )
				OnSeparation( (Entity)fixtureA.Body.UserData );
		}

		public virtual bool OnCollision( Entity other, Contact contact ) {
			return true;
		}

		public virtual void OnSeparation( Entity other ) {}
		
		public override void Remove() {
			if( IsMarkedForRemoval )
				return;

			IsMarkedForRemoval = true;
			World.RemovedEntities.Add( ID, this );

			if( PhysicsObject != null )
				World.Instance.RemoveBody( PhysicsObject );
		}
		#endregion

		public Vector2 LocalToWorld( Vector2 local ) {
			if( PhysicsObject != null )
				return PhysicsObject.GetWorldVector( local );
			return Position + local;
		}

		public Vector2 WorldToLocal( Vector2 world ) {
			if( PhysicsObject != null )
				return PhysicsObject.GetLocalVector( world );
			return Position - world;
		}
		
		#region Damage Handling
		/// <summary>
		/// Handles damaging the entity.
		/// </summary>
		/// <param name="amount">Amount of damage to inflict.</param>
		/// <param name="instigator">The person or entity who inflicted the damage.</param>
		/// <param name="inflictor">The "weapon" or instigator from above that inflicted the damage.</param>
		/// <param name="pos">Origin of the damage.</param>
		/// <param name="dir">Direction from the origin of the damage.</param>
		public void TakeDamage( float amount, Entity instigator, Entity inflictor, Vector2 pos, Vector2 dir ) {
			if( IsMarkedForRemoval || Invulnerable )
				return;

			// Instead of making a custom override version of TakeDamage I put this here
			if( this is Pawn ) {
				// Pass through items in inventory for items that adjust the damage (Shields, armor, etc.)
				foreach(Item item in ((Pawn)this).Inventory) {
					amount = item.OnOwnerTakeDamage( amount, instigator, inflictor, pos, dir );
				}
			}

			Health -= amount;
			OnTakeDamage( amount, instigator, inflictor, pos, dir );

			if( Health <= 0f ) {
				OnDeath( instigator, inflictor );
				Remove();
			}
		}

		public virtual void OnTakeDamage( float amount, Entity instigator, Entity inflictor, Vector2 pos, Vector2 dir ) {
		}

		public virtual void OnDeath( Entity instigator, Entity inflictor ) {
		}
		#endregion

		// ----------

		public EntitySaveState LoadedSaveState { get; set; }

		public Guid ID { get; set; }
		public Body PhysicsObject { get; private set; }
		public Texture2D MaskTexture { get; set; }

		public Entity Owner { get; set; }
		public Guid OwnerID {
			get { return (Owner != null ? Owner.ID : Guid.Empty); }
			set {
				Entity buffer = null;
				if( !World.AddedEntities.TryGetValue(value,out buffer) )
					World.Entities.TryGetValue( value, out buffer );
				Owner = buffer;
			}
		}
		
		public Guid ParentID {
			get { return (Parent != null && Parent is Entity ? ((Entity)Parent).ID : Guid.Empty); }
			set {
				Entity buffer = null;
				if( !World.AddedEntities.TryGetValue(value,out buffer) )
					World.Entities.TryGetValue( value, out buffer );
				Parent = buffer;
			}
		}

		public float Health { get; set; }
		public float MaxHealth { get; set; }
		public bool Invulnerable { get; set; }
		public bool IsAlive { get { return Health > 0f; } }

		public override Vector2 Position {
			get { return (PhysicsObject != null ? PhysicsObject.Position : base.Position); }
			set { if( PhysicsObject != null ) PhysicsObject.Position = value; else base.Position = value; }
		}

		public override Vector2 Velocity {
			get { return (PhysicsObject != null ? PhysicsObject.LinearVelocity : base.Velocity); }
			set { if( PhysicsObject != null ) PhysicsObject.LinearVelocity = value; else base.Velocity = value; }
		}

		public override float Angle {
			get { return (PhysicsObject != null ? PhysicsObject.Rotation : base.Angle); }
			set { if( PhysicsObject != null ) PhysicsObject.Rotation = value; else base.Angle = value; }
		}

		public override float AngularVelocity {
			get { return (PhysicsObject != null ? PhysicsObject.AngularVelocity : base.AngularVelocity); }
			set { if( PhysicsObject != null ) PhysicsObject.AngularVelocity = value; else base.AngularVelocity = value; }
		}
		
		public override float LinearDamping {
			get { return (PhysicsObject != null ? PhysicsObject.LinearDamping : base.LinearDamping); }
			set { if( PhysicsObject != null ) PhysicsObject.LinearDamping = value; else base.LinearDamping = value; }
		}

		public override float AngularDamping {
			get { return (PhysicsObject != null ? PhysicsObject.AngularDamping : base.AngularDamping); }
			set { if( PhysicsObject != null ) PhysicsObject.AngularDamping = value; else base.AngularDamping = value; }
		}

		public float Friction {
			get { return (PhysicsObject != null ? PhysicsObject.Friction : 0f); }
			set { if( PhysicsObject != null ) PhysicsObject.Friction = value; }
		}

		public float Inertia {
			get { return (PhysicsObject != null ? PhysicsObject.Inertia : 0f); }
			set { if( PhysicsObject != null ) PhysicsObject.Inertia = value; }
		}

		public float Mass {
			get { return (PhysicsObject != null ? PhysicsObject.Mass : 0f); }
			set { if( PhysicsObject != null ) PhysicsObject.Mass = value; }
		}

		public float Restitution {
			get { return (PhysicsObject != null ? PhysicsObject.Restitution : 0f); }
			set { if( PhysicsObject != null ) PhysicsObject.Restitution = value; }
		}

		public bool IsAwake {
			get { return (PhysicsObject != null ? PhysicsObject.Awake : false); }
			set { if( PhysicsObject != null ) PhysicsObject.Awake = value; }
		}

		public bool IsBullet {
			get { return (PhysicsObject != null ? PhysicsObject.IsBullet : false); }
			set { if( PhysicsObject != null ) PhysicsObject.IsBullet = value; }
		}

		public Vector2 LocalCenter {
			get { return (PhysicsObject != null ? PhysicsObject.LocalCenter : base.Position); }
		}

		public Vector2 WorldCenter {
			get { return (PhysicsObject != null ? PhysicsObject.WorldCenter : base.Position); }
		}

		// ----------

		public void EmitSound( string path ) {
			if( path[0] == '@' ) {
				// Script
				SoundScript snd;
				if( Main.SoundScripts.TryGetValue(path.Substring(1),out snd) )
					snd.Play();
				else
					Main.SoundEffects["Missing"].Play();
			} else {
				// Sound
				SoundEffect snd;
				if( Main.SoundEffects.TryGetValue(path,out snd) )
					snd.Play();
				else
					Main.SoundEffects["Missing"].Play();
			}
		}
	}

	// ----------------------------------------------------------------------------------------------------

	public class EntitySaveState {
		public EntitySaveState() {}

		public EntitySaveState( Entity ent ) {
			Data = new SerializableDictionary<string,object>();

			EntityType = ent.GetType();
			EntityID = ent.ID;
			ent.CreateSaveState( this );
		}

		// -----

		#region Helper Functions
		public bool Add( string key, object value ) {
			if( Data.ContainsKey(key) )
				return false;
			Data.Add( key, value );
			return true;
		}

		public bool Remove( string key ) {
			return Data.Remove( key );
		}

		public bool TryGetValue( string key, out object value ) {
			return Data.TryGetValue( key, out value );
		}

		public object this[string key] {
			get {
				return Data[key];
			}
			set {
				Data[key] = value;
			}
		}
		#endregion

		// -----

		[XmlIgnore]public Type EntityType { get; set; }
		public string EntityTypeName {
			get { return EntityType.FullName; }
			set { EntityType = Type.GetType( value ); }
		}

		public Guid EntityID { get; set; }

		public SerializableDictionary<string,object> Data { get; set; }
	}
}
